/**
 @file command.cpp

 @brief Implements the command class.
 */

#include "command.h"

#include "cmd-undocumented.h"
#include "commandparser.h"
#include "console.h"
#include "expressionparser.h"
#include "value.h"
#include "variable.h"

COMMAND* cmd_list = 0;

static bool vecContains(std::vector<String>* names, const char* name) {
  for (const auto& cmd : *names)
    if (!_stricmp(cmd.c_str(), name)) return true;
  return false;
}

/**
\brief Finds a ::COMMAND in a command list.
\param [in] command list.
\param name The name of the command to find.
\param [out] Link to the command.
\return null if it fails, else a ::COMMAND*.
*/
COMMAND* cmdfind(const char* name, COMMAND** link) {
  COMMAND* cur = cmd_list;
  if (!cur->names) return 0;
  COMMAND* prev = 0;
  while (cur) {
    if (vecContains(cur->names, name)) {
      if (link) *link = prev;
      return cur;
    }
    prev = cur;
    cur = cur->next;
  }
  return 0;
}

bool IsArgumentsLessThan(int argc, int minimumCount) {
  if (argc < minimumCount) {
    dprintf(
        QT_TRANSLATE_NOOP(
            "DBG",
            "Not enough arguments! At least %d arguments must be specified.\n"),
        minimumCount - 1);
    return true;
  }
  return false;
}

/**
\brief Initialize a command list.
\return a ::COMMAND*
*/
COMMAND* cmdinit() {
  cmd_list = (COMMAND*)emalloc(sizeof(COMMAND), "cmdinit:cmd");
  memset(cmd_list, 0, sizeof(COMMAND));
  return cmd_list;
}

/**
\brief Clear a command list.
\param [in] cmd_list Command list to clear.
*/
void cmdfree() {
  COMMAND* cur = cmd_list;
  while (cur) {
    delete cur->names;
    COMMAND* next = cur->next;
    efree(cur, "cmdfree:cur");
    cur = next;
  }
}

/**
\brief Creates a new command and adds it to the list.
\param [in,out] command_list Command list. Cannot be null.
\param name The command name.
\param cbCommand The command callback.
\param debugonly true if the command can only be executed in a debugging
context. \return true if the command was successfully added to the list.
*/
bool cmdnew(const char* name, CBCOMMAND cbCommand, bool debugonly) {
  if (!cmd_list || !cbCommand || !name || !*name || cmdfind(name, 0))
    return false;
  COMMAND* cmd;
  bool nonext = false;
  if (!cmd_list->names) {
    cmd = cmd_list;
    nonext = true;
  } else
    cmd = (COMMAND*)emalloc(sizeof(COMMAND), "cmdnew:cmd");
  memset(cmd, 0, sizeof(COMMAND));
  cmd->names = new std::vector<String>;
  auto split = StringUtils::Split(name, ',');
  for (const auto& s : split) {
    auto trimmed = StringUtils::Trim(s);
    if (trimmed.length()) cmd->names->push_back(trimmed);
  }
  cmd->cbCommand = cbCommand;
  cmd->debugonly = debugonly;
  COMMAND* cur = cmd_list;
  if (!nonext) {
    while (cur->next) cur = cur->next;
    cur->next = cmd;
  }
  return true;
}

/**
\brief Gets a ::COMMAND from the command list.
\param [in] command_list Command list.
\param cmd The command to get from the list.
\return null if the command was not found. Otherwise a ::COMMAND*.
*/
COMMAND* cmdget(const char* cmd) {
  char new_cmd[deflen] = "";
  strcpy_s(new_cmd, deflen, cmd);
  int len = (int)strlen(new_cmd);
  int start = 0;
  while (start < len && new_cmd[start] != ' ') start++;
  new_cmd[start] = 0;
  COMMAND* found = cmdfind(new_cmd, 0);
  if (!found) return 0;
  return found;
}

/**
\brief Sets a new command callback and debugonly property in a command list.
\param [in] command_list Command list.
\param name The name of the command to change.
\param cbCommand The new command callback.
\param debugonly The new debugonly value.
\return The old command callback.
*/
CBCOMMAND cmdset(const char* name, CBCOMMAND cbCommand, bool debugonly) {
  if (!cbCommand) return 0;
  COMMAND* found = cmdfind(name, 0);
  if (!found) return 0;
  CBCOMMAND old = found->cbCommand;
  found->cbCommand = cbCommand;
  found->debugonly = debugonly;
  return old;
}

/**
\brief Deletes a command from a command list.
\param [in] command_list Command list.
\param name The name of the command to delete.
\return true if the command was deleted.
*/
bool cmddel(const char* name) {
  COMMAND* prev = 0;
  COMMAND* found = cmdfind(name, &prev);
  if (!found) return false;
  delete found->names;
  if (found == cmd_list) {
    COMMAND* next = cmd_list->next;
    if (next) {
      memcpy(cmd_list, cmd_list->next, sizeof(COMMAND));
      cmd_list->next = next->next;
      efree(next, "cmddel:next");
    } else
      memset(cmd_list, 0, sizeof(COMMAND));
  } else {
    prev->next = found->next;
    efree(found, "cmddel:found");
  }
  return true;
}

bool cbCommandProvider(char* cmd, int maxlen);

void cmdsplit(const char* cmd, StringList& commands) {
  commands.clear();
  auto len = strlen(cmd);
  auto inquote = false;
  auto inescape = false;
  std::string split;
  split.reserve(len);
  for (size_t i = 0; i < len; i++) {
    auto ch = cmd[i];
    switch (ch)  // simple state machine to determine if the ';' separator is in
                 // quotes
    {
      case '\"':
        if (!inescape) inquote = !inquote;
        inescape = false;
        break;
      case '\\':
        inescape = !inescape;
        break;
      default:
        inescape = false;
    }
    if (ch == ';' && !inquote) {
      if (!split.empty()) commands.push_back(split);
      split.clear();
    } else
      split.push_back(ch);
  }
  if (!split.empty()) commands.push_back(split);
}

bool cmdexeccallback(COMMAND* cmd, const std::string& command) {
  Command commandParsed(command);
  int argcount = commandParsed.GetArgCount();
  char** argv = (char**)emalloc((argcount + 1) * sizeof(char*), "cmdloop:argv");
  argv[0] = (char*)command.c_str();
  for (int i = 0; i < argcount; i++) {
    argv[i + 1] = (char*)emalloc(deflen, "cmdloop:argv[i+1]");
    *argv[i + 1] = 0;
    strcpy_s(argv[i + 1], deflen, commandParsed.GetArg(i).c_str());
  }
  auto res = cmd->cbCommand(argcount + 1, argv);
  for (int i = 0; i < argcount; i++) efree(argv[i + 1], "cmdloop:argv[i+1]");
  efree(argv, "cmdloop:argv");
  return res;
}

/**
\brief Initiates the command loop. This function will not return until a command
returns ::STATUS_EXIT. \return A bool, will always be ::STATUS_EXIT.
*/
void cmdloop() {
  char command_[deflen] = "";
  StringList commands;
  commands.reserve(100);
  while (true) {
    if (!cbCommandProvider(command_, deflen)) break;
    cmdsplit(command_, commands);
    for (auto& command : commands) {
      command = StringUtils::Trim(command);
      if (command.empty())  // skip empty commands
        continue;

      COMMAND* found = cmdget(command.c_str());
      if (!found || !found->cbCommand)  // unknown command
      {
        char* argv[1];
        *argv = (char*)command.c_str();
        if (!cbBadCmd(1, argv))  // stop processing on non-value commands
          break;
        continue;
      }

      if (found->debugonly &&
          !DbgIsDebugging())  // stop processing on debug-only commands
      {
        dprintf(QT_TRANSLATE_NOOP("DBG", "The command \"%s\" is debug-only\n"),
                command.c_str());
        break;
      }

      // execute command callback
      if (!cmdexeccallback(found, command)) break;
    }
  }
}

/**
\brief Directly execute a command.
\param [in,out] cmd_list Command list.
\param cmd The command to execute.
\return A bool.
*/
bool cmddirectexec(const char* cmd) {
  // Don't allow anyone to send in empty strings
  if (!cmd) return false;

  StringList commands;
  cmdsplit(cmd, commands);
  for (auto& command : commands) {
    command = StringUtils::Trim(command);
    if (command.empty())  // skip empty commands
      continue;

    COMMAND* found = cmdget(command.c_str());
    if (!found || !found->cbCommand)  // unknown command
    {
      ExpressionParser parser(command);
      duint result;
      if (!parser.Calculate(result, valuesignedcalc(), true,
                            false))  // stop processing on non-value commands
        return false;
      varset("$ans", result, true);
      continue;
    }

    if (found->debugonly &&
        !DbgIsDebugging())  // stop processing on debug-only commands
    {
      dprintf(QT_TRANSLATE_NOOP("DBG", "The command \"%s\" is debug-only\n"),
              command.c_str());
      return false;
    }

    // execute command callback
    if (!cmdexeccallback(found, command)) return false;
  }
  return true;
}
